package dryven.model.binding.proxy;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;

import dryven.codegen.asm.ExceptionScope;
import dryven.codegen.asm.Access;
import dryven.codegen.asm.ClassBuilder;
import dryven.codegen.asm.ExceptionHandler;
import dryven.codegen.asm.FieldDefinition;
import dryven.codegen.asm.Literal;
import dryven.codegen.asm.LocalVariable;
import dryven.codegen.asm.MethodBuilder;
import dryven.codegen.asm.Scope;
import dryven.codegen.asm.TempValue;
import dryven.codegen.asm.TempValuePreparer;
import dryven.codegen.asm.TryCatchBlock;
import dryven.codegen.asm.typeinfo.ClassTypeInfo;
import dryven.codegen.asm.typeinfo.UncompiledClassTypeInfo;

public class CallInterceptorStubClassLoader extends ClassLoader {

	public CallInterceptorStubClassLoader(ClassLoader parent) {
		super(parent);
	}
	
	@Override
	protected Class<?> findClass(String name) throws ClassNotFoundException {
		if(name.endsWith(getSuffix())) {
			String superClassName = name.substring(0, name.length()-getSuffix().length());
			Class<?> superClass = loadClass(superClassName);
			
			byte[] byteCode = generateByteCode(superClass);
			return defineClass(byteCode, 0, byteCode.length);
		}
		return super.findClass(name);
	}

	public String getProxyClassName(Class<?> clazz) {
		return clazz.getName()+getSuffix();
	}
	
	private String getSuffix() {
		return "DryvenCallInterceptorStub__";
	}
	
	private byte[] generateByteCode(Class<?> superClass) {
		String className = superClass.getName()+getSuffix();
		
		ClassBuilder cBuilder = new ClassBuilder(className, superClass);
		
		FieldDefinition _interceptor = cBuilder.buildField("_interceptor", Access.Private, CallInterceptor.class);
		
		buildConstructor(superClass, cBuilder, _interceptor);
		//override all declared methods polymorphic (public or protected) in the superclass
		for (Method m : superClass.getDeclaredMethods()) {
			if((m.getModifiers() & (Modifier.PUBLIC | Modifier.PROTECTED)) != 0) {
				Access access = Access.Public;
				if((m.getModifiers() & Modifier.PROTECTED) != 0) {
					access = Access.Protected;
				}
				if(m.getReturnType().equals(Void.TYPE)) {
					generateSetter(cBuilder, m, access);
				} else {
					generateGetter(cBuilder, _interceptor, m, access);
				}
			}
		}
		byte[] bcode = cBuilder.finish();
		return bcode;
	}

	private void generateSetter(ClassBuilder cBuilder, Method m, Access access) {
		//you can't bind a setter, so always throw an exception
		MethodBuilder setter = cBuilder.buildMethod(m.getName(), access, Void.TYPE, m.getParameterTypes());
		setter.throwException(setter.newObject(UnsupportedOperationException.class));
		setter.finish();
	}

	private void generateGetter(ClassBuilder cBuilder, final FieldDefinition _interceptor,
			final Method m, Access access) {
		//create method header
		MethodBuilder getter = cBuilder.buildMethod(m.getName(), access, m.getReturnType(), m.getParameterTypes());
		TryCatchBlock tcb1 = new TryCatchBlock(getter);
		ExceptionHandler handler1 = tcb1.addExceptionHandler(Exception.class);
		//start try block
		tcb1.start(new Scope() {
			@Override
			public void addCode() {
				MethodBuilder getter = getBuilder();
				//call methodCalled on the interceptor field. Use a TempValuePreparer to not have to use local variables but only TempValue's
				getter.getThis().getFieldInstance(_interceptor).read().invoke("methodCalled", Void.TYPE, new TempValuePreparer(getter) {
					@Override
					public void addValues() {
						//SuperClass.class ...
						Literal clazz = new Literal(m.getDeclaringClass(), getBuilder());
						// ... getDeclaredMethod(...);
						TempValue method = clazz.read().invoke("getDeclaredMethod", Method.class, new TempValuePreparer(getBuilder()) {
							@Override
							public void addValues() {
								pushParameter(new Literal(m.getName(),getBuilder()).read());
								TempValue parameters = getBuilder().newArray(Class.class, m.getParameterTypes().length);
								for(int i=0;i<m.getParameterTypes().length;++i) {
									parameters.duplicate().getArrayEntry(i).assign(new Literal(m.getParameterTypes()[i], getBuilder()));
								}
								pushParameter(parameters);
							}
						});
						//push the received method as an argument for the call to interceptor
						this.pushParameter(method);
						//now make an array with all the passed parameters to this method invocation
						TempValue parameters = getBuilder().newArray(Object.class, m.getParameterTypes().length);
						for(int i=0;i<getBuilder().getParameterCount();++i) {
							LocalVariable param = getBuilder().getParameter(i);
							TempValue arrayDup = parameters.duplicate();
							TempValue autoboxedValue = getBuilder().autoBox(param);
							arrayDup.getArrayEntry(i).assign(autoboxedValue);
						}
						//and also add it as a parameter to call interceptor
						this.pushParameter(parameters);
					}
				});
			}
		});
		//handle any possible exception ...
		handler1.start(new ExceptionScope() {
			@Override
			public void addCode() {
				MethodBuilder getter = getBuilder();
				//and rethrow them ...
				LocalVariable ex = getter.newLocalVariable(Throwable.class);
				ex.assign(getException());
				getter.throwException(getter.newObject(RuntimeException.class, new Literal("Could not get method via reflection",getter), ex));
			}
		});
		//create a default literal value for this kind of return type
		Literal l = getter.createDefaultValueForType(new ClassTypeInfo(m.getReturnType()));
		//and return it
		getter.returnValue(l);
		getter.finish();
	}

	private void buildConstructor(Class<?> superClass, ClassBuilder cBuilder,
			FieldDefinition _interceptor) {
		//build a constructor with only one argument, a CallInterceptor
		MethodBuilder ctor = cBuilder.buildConstructor(Access.Public, CallInterceptor.class);
		//using java reflection, look for the constructor with the fewest arguments
		Constructor<?> parentConstructor = findShortestConstructorForSuperClass(superClass);
		//make an array with a default value for all parameters depending on their type
		Literal parentConstructorParams[] = new Literal[parentConstructor.getParameterTypes().length];
		for(int i=0;i<parentConstructor.getParameterTypes().length;++i) {
			parentConstructorParams[i] = ctor.createDefaultValueForType(new ClassTypeInfo(parentConstructor.getParameterTypes()[i]));
		}
		//call the constructor
		ctor.callSuperConstructor(parentConstructorParams);
		//assign _interceptor
		ctor.getThis().getFieldInstance(_interceptor).assign(ctor.getParameter(0).read());
		//finish constructor
		ctor.returnVoid();
		ctor.finish();
	}
	
	private Constructor<?> findShortestConstructorForSuperClass(Class<?> currentSuperClass) {
		//first find the constructor with the least amount of arguments
		int minArgumentCount = Integer.MAX_VALUE;
		Constructor<?> minCtor = null;
		for (Constructor<?> ctor : currentSuperClass.getDeclaredConstructors()) {
			if(ctor.getParameterTypes().length<minArgumentCount) {
				minArgumentCount = ctor.getParameterTypes().length;
				minCtor = ctor;
			}
		}
		return minCtor;
	}
}
